<template>
  <div class="viewport" ref="viewport">
    <div class="placeholder"
    :style="{height: contentHeight}"
    ></div>
    <RecycleScroller 
      :items="allVisibleData"
      :item-size="option.itemHeight"
      key-field="id"
      v-slot="{item}"
      class="scroller"
    >
      <li 
        :key="item.path + item.name"
        @click="handleNodeClick(item)"
        :style="{paddingLeft: `${item.level * 5}px`}"
        :draggable="false"
        @mousedown="onMousedown($event, item)"
        :attrs-path="item.name"
        class="tree-item-content"
      > 
        <label 
          class="checkbox" 
          v-if="option.showCheckbox" 
          @click.stop>
          <span class="checkbox__input"
          :class="
            [
              item.isChecked ? 'is-checked' : '', 
              item.isHalfChecked ? 'is-indeterminate' : '',
            ]
          "
          :for="item.path + item.name"
          >
            <span class="checkbox__inner"></span>
            <input 
              class="checkbox__original" 
              type="checkbox"
              :checked="item.isChecked" 
              :indeterminate="item.isHalfChecked"
              @change.stop="handleCheckChange(item)"
              :id="item.path + item.name"
            >
          </span>
        </label>
        <i class="el-icon-folder"></i>
        <i v-if="item.showChildren" class="el-icon-caret-bottom"></i>
        <i v-else class="el-icon-caret-right"></i>
        <span>{{ item.name }}</span>
      </li>
    </RecycleScroller>
  </div>
</template>

<script>
import draggable from 'vuedraggable'
import {
  RecycleScroller
} from 'vue-virtual-scroller'
export default {
  name: 'virtualTree',
  components: {
    draggable,
    RecycleScroller
  },
  data () {
    return {
      treeData: [],
      allVisibleData: [], // 所有可以展示的数据，父节点的showChildren是true，那么子节点的visible就是true，表示可以展示，
      visibleData: [], // 
      scrollValue: 0,
      flattenTree: [], 
      offset: [],
      contentHeight: 0, // 占位容器布局高度
      start: 0, // 开始索引
      end: 0, // 结束索引
      clientX: 0,
      clientY: 0,
      isMove: false,
      dragItem: null, // 开始拖拽的对象
      dragParent: '', // 拖拽父亲的名称
      refreshTimeout: null, // 滚动定时器
      _scrollDirty: false
    }
  },
  props: {
    option: {
      type: Object,
      default: () => {}
    }
  },
  computed: {
    allVisibleDataLength () {
      return this.allVisibleData.length
    }
  },
  watch: {
    allVisibleDataLength: {
      handler (newVal, oldVal) {
        if (newVal) {
          // 监听可视数据的变化，去更新占位符的高度，展开或者折叠的时候滚动条发生变化
          this.updatePlaceHolderHeight()
        }
      },
      deep: true
    }
  },
  created () {
    this.buildData()
  },
  mounted () { },
  methods: {
    onMousedown (event, item) {
      // 拿到小球的logo
      let ball = document.getElementById('drag-logo');
      // 修改name
      ball.querySelector('span.from').innerText = item.name + '移动到';
      // 设置鼠标的定位
      ball.style.position = 'absolute';
      ball.style.zIndex = 1000;
      // 移动
      this.moveAt(event.clientX, event.clientY)
      this.clientY = event.clientY;
      this.clientX = event.clientX;
      // 开始拖动监听鼠标事件
      document.addEventListener('mousemove', this.onMouseMove)
      document.addEventListener('mouseup', this.onMouseUp)
      // 开始拖动的对象
      this.dragItem = item
    },
    onMouseMove (event) {
      if (this.clientX === 0 && this.clientY === 0) {
        return 
      }
      let distance = Math.sqrt(Math.pow(event.clientX - this.clientX, 2) + Math.pow(event.clientY - this.clientY, 2)) 
      console.log(distance, 'distance');
      if (distance > 8) {
        this.isMove = true
        let ball = document.getElementById('drag-logo');
        // 这里才显示
        ball.style.opacity = 1
        // 鼠标经过的dom
        let target = document.elementFromPoint(event.clientX, event.clientY); 
        // 找到指定子元素的父元素li标签
        let parent = this.getElementParent(target, 'tree-item-content') 
        // 拿到目标的target的dom元素

        if (parent) ball.querySelector('span.to').innerText = parent.getAttribute('attrs-path')
        // 时刻更新鼠标跟随提示
        this.moveAt(event.clientX, event.clientY)
      }
    },
    getElementParent (node, parentName) {
      if (!node) return null
      // 就是当前目录
      if (node.className && node.className.includes(parentName)) {
        return node
      }
      let parent = node.parentNode
      // 拿到父级目录
      let flag = parent && parent.className && parent.className.includes(parentName)
      if (flag) {
        return parent
      } else {
        // 递归
        this.getElementParent(parent, parentName)
      }
    },
    onMouseUp (event) {
      let ball = document.getElementById("drag-logo");
      ball.querySelector("span.from").innerText = ""
      ball.querySelector("span.to").innerText = ""
      ball.style.opacity = 0;
      document.removeEventListener('mousemove', this.onMouseMove);
      document.removeEventListener('mouseup', this.onMouseUp);
      

      if (this.isMove) {
        // 在指定dom上松开的
        let target = document.elementFromPoint(event.clientX, event.clientY); 
        // 找到指定子元素的父元素li标签
        let toItem = this.getElementParent(target, 'tree-item-content') 
        // 更新数据
        this.updateDragData(toItem)
      }

      // 鼠标每次松开，都要取消移动
      this.isMove = false
      // 鼠标左键松开，置空点击的按钮
      this.clientX = 0
      this.clientY = 0
    },
    // 在这里更新数据
    updateDragData (toItem) {
      // 提前获取拖拽对象的数据和父节点
      let {element, parent} = this.getChildAndParentTree(this.treeData, this.dragItem.name)
      this.dragParent = parent
      if (!parent) {
        // 若没有parent则是第一级节点，直接删除顶层数据,通过name找到数据
        const spliceIndex = this.treeData.findIndex(item => item.name === element.name)
        // 删除
        this.treeData.splice(spliceIndex, 1)
      } else {
        // 有parent表示是第二、三及以上的节点
        const spliceIndex = parent.children.findIndex(item => item.name === element.name)
        parent.children.splice(spliceIndex, 1)
      }
      // 拿到目标的name值
      let toName = toItem.getAttribute('attrs-path')
      // 找到目标的dom
      let toTarget = this.getChildAndParentTree(this.treeData, toName)
      // push到目标的children中，成为他的子数据
      toTarget.element.children.push(this.dragItem)
      // 更新拍平的数据
      this.flattenTree = this.flatten(this.treeData)
      // 手动触发点击方法，展开to节点
      this.handleNodeClick(toTarget.element)
      // 手动展开from节点
      this.handleNodeClick(this.dragParent)
      // 置空拖拽对象
      this.dragItem = null
    },
    getChildAndParentTree (arr, targetName, parent = null) {
      for (let i = 0; i < arr.length; i++) {
        let currentObj = arr[i]
        // 当前项就是该数据
        if (currentObj.name === targetName) {
          return {
            element: currentObj,
            parent
          }
        }
        // 遍历子children
        if (currentObj.children && currentObj.children.length > 0) {
          let result = this.getChildAndParentTree(
            currentObj.children,
            targetName,
            currentObj
          )
          if (result && result.element) {
            return result
          }
        }
      }
    },
    moveAt (clientX, clientY) {
      let ball = document.getElementById("drag-logo");
      ball.style.left = clientX + 15 + 'px'
      ball.style.top = clientY + 'px'
    },
    handleScroll () {
      if (this.$refs.viewport) {
        this.scrollValue = this.$refs.viewport.scrollTop
        if (!this._scrollDirty) {
          this._scrollDirty = true
          this.debounceUpdateVisibleData(this.scrollValue)
        }
      }
    },
    debounce (func, delay) {
      const context = this
      return function (...args) {
        clearTimeout(context.refreshTimeout)
        context.refreshTimeout = setTimeout(() => func.apply(context, args), delay)
      }
    },
    debounceUpdateVisibleData(scrollValue) {
      let toolFn =  () => {
        requestAnimationFrame(() => {
          this._scrollDirty = false
          this.updateVisibleData(scrollValue)
        })
      }
      let fn = this.debounce(toolFn, 100)
      fn()
    },
    // 1. 构建数据
    buildData () {
      const data = [],
        root = 30,
        children = 3,
        base = 500;
      for (let i = 0; i < root; i++) {
        data.push({
          id: `${i}`,
          path: `/${i}`,
          name: `test-${i}`,
          children: [],
          showChildren: false,
        });
        for (let j = 0; j < children; j++) {
          data[i].children.push({
            id: `${i}-${j}`,
            path: `/${i}-${i}`,
            name: `test-${i}-${j}`,
            children: [],
            showChildren: false,
          });
          for (let k = 0; k < base; k++) {
            data[i].children[j].children.push({
              id: `${i}-${j}-${k}`,
              path: `/${i}-${j}-${k}`,
              name: `test-${i}-${j}-${k}`,
              showChildren: false,
              children: [],
            });
          }
        }
      }
      this.treeData = data;
      // 2. 拍平数据
      this.flattenTree = this.flatten(this.treeData)
      // 3. 更新可视区域的数据
      this.updateVisibleData()
    },
    // 拍平
    flatten (
      list = [], // 要排名的数据
      level = 1, // 树的深度
      parent = null, // 父节点的数据
      defaultExpand = false // 控制每一层是展开还是关闭
    ) {
      let arr = []
      for (let i = 0; i < list.length; i++) {
          let item = list[i]
          // 设置深度
          this.$set(item, 'level', level) 
          // 默认所有的节点都不展开，不显示子节点
          this.$set(item, 'showChildren', defaultExpand) 
          if (item.visible === undefined && item.level > 1) {
              // visible变量用于控制是否显示，
              // 我们的原理就是每次只展示固定数量的节点
              // 有的节点隐藏就是通过这个变量判断
              // 我们让层级大于1的都隐藏
              this.$set(item, 'visible', false) 
          } else if (item.visible === undefined) {
              // 层级是1的显示
              this.$set(item, 'visible', true) 
          }

          // 设置勾选框
          if (this.option.showCheckbox) {
            this.$set(item, 'isChecked', false)
            this.$set(item, 'isHalfChecked', false)
          }

          // 设置parent的值
          this.$set(item, 'parent', parent) 
          // 把当前的item的值放到arr中
          arr.push(item)

          
          // 递归遍历
          if (item['children'] && item['children'].length > 0) {
              arr.push(
                  ...this.flatten(
                      item['children'],
                      level + 1, // 树的深度递归就 + 1
                      item, // 当前的item用作父亲
                      defaultExpand // 控制每一层是展开还是关闭 
                  )
              )
          }
      }
      return arr
    },
    handleCheckChange (node) {
      try {
        // 如果是半选，修改为全选
        if (node.isHalfChecked) {
          this.$set(node, 'isChecked', true)
        } else if (node.isChecked) {
          // 如果是全选，修改为非全选
          this.$set(node, 'isChecked', false)
        } else {
          // 如果是非全选，修改为全选
          this.$set(node, 'isChecked', true)
        }
        // 只会有孩子选中时（handleCheckChildren）触发isHalfChecked的修改，其他都改为false
        this.$set(node, 'isHalfChecked', false)
        // 处理子目录和子ipc
        this.handleCheckChildren(node)
        // 处理父目录
        this.handleCheckParent(node.parent)
      } catch (error) {
        console.log(error, 'error');
      }
    },
    handleCheckChildren (node) {
      node.children && node.children.forEach(child => {
        // 点击当前目录，修改子目录，只会影响子目录的全选或者全不选，半选统一为false
        this.$set(child, 'isChecked', node.isChecked)
        this.$set(child, 'isHalfChecked', false)
        if (child.children) {
          // 递归遍历子目录
          this.handleCheckChildren(child)
        }
      })
    },
    handleCheckParent (node) {
      // 初始值，假设所有子节点是checked,在后面的遍历中修改这个值
      let isAll = true 
      // 初始值，假设没有子节点是半选,在后面的遍历中修改这个值
      let isHalf = false 
      // 无name属性的目录不递归
      if (!node || !node.name) {
        return
      }
      // 根据所有的子目录判断是否全选
      if (node && node.children && node.children.length > 0) {
        // 如果子目录没有全部勾选，item.isChecked是false
        isAll = node.children.every(item => item.isChecked)
        // 如果子目录有一个是全选或者半选状态
        isHalf = node.children.some(item => item.isHalfChecked || item.isChecked)
        // 修改父目录选中
        node.isChecked = isAll
        // 全选和半选不能同时存在
        node.isHalfChecked = !isAll && isHalf 
      }
      // 递归影响父节点
      this.handleCheckParent(node.parent)
    },
    // 更新页面展示数据
    updateVisibleData (scrollValue = 0) {
      console.log(scrollValue, 'scrollValue');
      // 开始索引
      this.start = Math.floor(scrollValue / this.option.itemHeight)
      // 结束索引
      this.end = this.start + this.option.count * 3
      // 更新所有显示数组
      this.allVisibleData = (this.flattenTree || []).filter(item => item.visible)
      // 显示在页面上的数据
      this.visibleData = this.allVisibleData.slice(this.start, this.end)
      console.log(this.start, 'this.start');
      // ul列表的Y轴位移
      this.offset = this.start * this.option.itemHeight
    },
    updateView () {
      // 更新非折叠数据
      this.allVisibleData = (this.flattenTree || []).filter(item => item.visible)
      // 更新最终显示的数据 --- 传递scrollValue
      this.updateVisibleData(this.scrollValue)
    },
    // 点击某个节点
    handleNodeClick (item) {
      // 展开或者关闭取反
      this.$set(item, 'showChildren', !item.showChildren)
      // 递归修改子元素的visible
      this.recursionVisible(item, item.showChildren)
      // 更新视图
      this.updateView()
    },
    // 控制子节点展开与否
    recursionVisible (data, status) {
      // 打开子节点第一层
      if (status) {
        data.children.forEach(node => {
          node.visible = status
        })
      }
      // 关闭并折叠所有子系欸但
      else {
        data.children.forEach(node => {
          node.visible = status
          node.showChildren = false
          if (node.children) {
            this.recursionVisible(node, status)
          }
        })
      }
    },
    // 更新展位容器高度
    updatePlaceHolderHeight() {
      this.contentHeight = (this.flattenTree || []).filter(item => item.visible).length * this.option.itemHeight + 'px'
    },
  },
}
</script>
<style scoped lang='less'>
.viewport {
  width: 300px;
  height: 600px;
  // 记得这里设置相对定位
  position: relative;
  ul {
    width: 100%;
  }
  li {
    height: 30px;
    list-style: none;
    text-align: left;
    user-select: none;
  }
  .placeholder {
    position: absolute;
    top: 0;
    left: 0;
    // 记得左右上下都要设置，表示左右上下都撑开容器
    right: 0;
    z-index: -1;
  }
  .checkbox__original {
    width: 0;
    height: 0;
  }
  .checkbox__inner {
    display: inline-block;
    box-sizing: border-box;
    position: relative;
    border-radius: 2px;
    width: 14px;
    height: 14px;
    border: 1px solid #dcdfe6;
  }
  .checkbox__input.is-checked {
    .checkbox__inner {
      background-color: #409eff;
      border-color: #409eff;
      &::after {
        content: '';
        position: absolute;
        box-sizing: content-box;
        top: 1px;
        left: 4px;
        width: 3px;
        height: 7px;
        font-size: 12px;
        color: #fff;
        border: 1px solid #fff;
        border-left: 0;
        border-top: 0;
        transition: transform .15s ease-in .05s;
        transform-origin: center;
        transform: rotate(44deg) scaleY(1);
      }
    }
  }
  .checkbox__input.is-indeterminate {
    .checkbox__inner {
      background-color: #409eff;
      border-color: #409eff;
      &::after {
        content: '';
        position: absolute;
        box-sizing: content-box;
        left: 0;
        right: 0;
        top: 5px;
        height: 2px;
        transform: scale(.5);
        background-color: #fff;
      }
    }
  }
}
.scroller {
  height: 600px;
  overflow: auto;
}
</style>
